/**
 * Copyright (c) 2012-2017, www.tinygroup.org (luo_guo@icloud.com).
 * <p>
 * Licensed under the GPL, Version 3.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.gnu.org/licenses/gpl.html
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.tinygroup.parsedsql.impl;

import org.springframework.jdbc.core.*;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.rowset.SqlRowSet;
import org.tinygroup.context.Context;
import org.tinygroup.logger.LogLevel;
import org.tinygroup.logger.Logger;
import org.tinygroup.logger.LoggerFactory;
import org.tinygroup.parsedsql.JDBCNamedSqlExecutor;
import org.tinygroup.parsedsql.ParsedSql;
import org.tinygroup.parsedsql.ResultSetCallback;
import org.tinygroup.parsedsql.SQLParser;
import org.tinygroup.parsedsql.base.DatabaseType;
import org.tinygroup.parsedsql.util.NamedParameterUtils;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * @author ballackhui
 *
 */
public class SimpleJDBCNamedSqlExecutor implements JDBCNamedSqlExecutor {

    /** Default maximum number of entries for this template's SQL cache: 256 */
    public static final int DEFAULT_CACHE_LIMIT = 256;
    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleJDBCNamedSqlExecutor.class);
    private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
    /** Cache of original SQL String to ParsedSql representation */
    @SuppressWarnings("serial")
    private final Map<String, ParsedSql> parsedSqlCache = new LinkedHashMap<String, ParsedSql>(
            DEFAULT_CACHE_LIMIT, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, ParsedSql> eldest) {
            return size() > getCacheLimit();
        }
    };
    private SQLParser sqlParser;

    /**
     * Return the maximum number of entries for this template's SQL cache.
     */
    public int getCacheLimit() {
        return this.cacheLimit;
    }

    /**
     * Specify the maximum number of entries for this template's SQL cache.
     * Default is 256.
     */
    public void setCacheLimit(int cacheLimit) {
        this.cacheLimit = cacheLimit;
    }

    public SQLParser getSqlParser() {
        return sqlParser;
    }

    public void setSqlParser(SQLParser sqlParser) {
        this.sqlParser = sqlParser;
    }

    @Override
    public SqlRowSet queryForSqlRowSet(String sql, DataSource dataSource,
                                       Context context) throws SQLException {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        return jdbcTemplate.query(getPreparedStatementCreator(sql, dataSource, context),
                new SqlRowSetResultSetExtractor());
    }

    private PreparedStatementCreator getPreparedStatementCreator(String sql, DataSource dataSource,
                                                                 Context context) throws SQLException {
        if (sqlParser != null) {
            LOGGER.logMessage(LogLevel.DEBUG, "before SQLParser SQL：{0}", sql);
            DatabaseType databaseType = getDatabaseTypeWithDataSource(dataSource);
            sql = sqlParser.parse(databaseType, sql, context);
            LOGGER.logMessage(LogLevel.DEBUG, "after SQLParser SQL：{0}", sql);
        }
        SqlParameterSource paramSource = new MapSqlParameterSource(
                context.getTotalItemMap());
        ParsedSql parsedSql = getParsedSql(sql);
        String sqlToUse = NamedParameterUtils.substituteNamedParameters(
                parsedSql, paramSource);
        Object[] params = NamedParameterUtils.buildValueArray(parsedSql,
                paramSource, null);
        List<SqlParameter> declaredParameters = NamedParameterUtils
                .buildSqlParameterList(parsedSql, paramSource);
        PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(
                sqlToUse, declaredParameters);
        return pscf.newPreparedStatementCreator(params);
    }

    private DatabaseType getDatabaseTypeWithDataSource(DataSource dataSource)
            throws SQLException {
        String databaseTypeStr;
        Connection connection = dataSource.getConnection();
        try {
            databaseTypeStr = connection.getMetaData()
                    .getDatabaseProductName();
        } finally {
            if (connection != null) {
                connection.close();
            }
        }
        DatabaseType databaseType = DatabaseType.valueFrom(databaseTypeStr);
        return databaseType;
    }

    @Override
    public int execute(String sql, DataSource dataSource, Context context) throws SQLException {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        return jdbcTemplate.update(getPreparedStatementCreator(sql, dataSource, context));
    }

    protected ParsedSql getParsedSql(String sql) {
        if (getCacheLimit() <= 0) {
            return NamedParameterUtils.parseSqlStatement(sql);
        }
        synchronized (this.parsedSqlCache) {
            ParsedSql parsedSql = this.parsedSqlCache.get(sql);
            if (parsedSql == null) {
                parsedSql = NamedParameterUtils.parseSqlStatement(sql);
                this.parsedSqlCache.put(sql, parsedSql);
            }
            return parsedSql;
        }
    }

    @Override
    public void extractResultSetCallback(String sql, DataSource dataSource,
                                         Context context, ResultSetCallback callback) throws SQLException {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        jdbcTemplate.query(getPreparedStatementCreator(sql, dataSource, context),
                new TinyResultSetCallback(callback));
    }

}
